P04-2. Zero-Trust 보안과 SPIFFE SVID 인증 인가 - 실습 검증

Part 04 (실습): Zero-Trust 보안과 SPIFFE/SVID 인증 · 인가 검증

실습 목표

  1. 워커 노드의 ztunnel 메모리에 발급된 실제 SPIFFE X.509 인증서(SVID) 목록을 조회하고, spiffe://cluster.local/ns/<NS>/sa/<SA> 형식의 전자 여권 구조를 확인한다.
  2. PeerAuthentication (mode: STRICT) 정책을 배포해 네임스페이스 내 모든 평문 통신을 원천 차단하고 오직 mTLS(HBONE) 암호화 통신만 허용한다.
  3. 인가되지 않은 침입자 파드(rogue-client, 계정: rogue-sa)를 투입한 뒤, Waypoint Proxy가 집행하는 L7 AuthorizationPolicy 를 통해 계정별(principals), HTTP 메서드별(GET vs DELETE), URL 경로별(/orders vs /admin/secret) 출입 통제(200 OK vs 403 RBAC: access denied) 를 완벽히 검증한다.
  4. 파드 IP로 직접 찌르는 우회 공격을 막기 위해 ztunnel이 직접 집행하는 L4 AuthorizationPolicy 까지 적용하고 ztunnel의 실시간 차단 로그를 확인한다.

사전 환경 확인

  • P04-1 이론 개념을 먼저 읽고 오면 SPIFFE, PeerAuthentication, AuthorizationPolicy의 우선순위와 L4(ztunnel) vs L7(Waypoint) 집행 차이가 명확히 이해됩니다.
  • Part 03 실습을 마친 상태(part1-demo 네임스페이스에 Ambient 모드와 waypoint 프록시가 활성화된 상태)에서 바로 이어서 진행합니다.
  • Windows PowerShell 창을 열고 아래 명령어로 현재 파드 상태를 확인합니다.
    $env:Path += ";$HOME\bin"
    cd "$HOME\mesh-study"
    kubectl get pods -n part1-demo
    


실습 아키텍처 및 Zero-Trust 검문 시나리오

이번 실습에서 검증할 4가지 출입 통제 시나리오입니다.

flowchart TB
    subgraph CLIENTS["요청을 보내는 두 파드 (서로 다른 SPIFFE 여권 보유)"]
        C_GOOD["study-client 파드
여권: spiffe://cluster.local/ns/part1-demo/sa/default"] C_BAD["rogue-client 파드 (침입자!)
여권: spiffe://cluster.local/ns/part1-demo/sa/rogue-sa"] end subgraph GATEKEEPER["L7 검문소 · Waypoint Proxy (order-api-zero-trust 정책)"] RULE["[허용 조건 (Whitelist)]
1. 출처(Principal): .../sa/default 만 허용
2. 메서드(Method): GET 만 허용
3. 금지 경로(notPaths): /admin* 제외"] end subgraph RESULTS["검문 결과"] R1["시나리오 1: study-client -> GET /orders
결과: 200 OK 통과!"] R2["시나리오 2: study-client -> GET /admin/secret
결과: 403 RBAC: access denied 차단!"] R3["시나리오 3: study-client -> DELETE /orders
결과: 403 RBAC: access denied 차단!"] R4["시나리오 4: rogue-client -> GET /orders
결과: 403 RBAC: access denied 차단!"] end C_GOOD --> RULE C_BAD --> RULE RULE --> R1 RULE --> R2 RULE --> R3 RULE --> R4

Step 1. ztunnel 메모리에 발급된 SPIFFE 전자 여권(SVID) 실물 확인

istiod 컨트롤 플레인(내장 CA)이 각 워커 노드의 ztunnel에게 어떤 디지털 인증서를 내려보냈는지 istioctl ztunnel-config certificates 명령어로 확인합니다.
(이때 어느 노드의 ztunnel을 조회할지 --node <노드이름> 옵션을 지정합니다.)

# 1. desktop-worker 노드의 ztunnel이 보유 중인 SPIFFE 인증서 목록 조회
istioctl ztunnel-config certificates --node desktop-worker

# 2. JSON 출력에서 SPIFFE ID(identity) 필드만 발췌하여 확인
istioctl ztunnel-config certificates --node desktop-worker -o json | Select-String "spiffe://" -Context 2,2

실제 출력 결과:

CERTIFICATE NAME                                     TYPE     STATUS        VALID CERT     SERIAL NUMBER                        NOT AFTER                NOT BEFORE
spiffe://cluster.local/ns/part1-demo/sa/default      Leaf     Available     true           18e2aac1fbba1042a49b622055891b69     2026-10-26T07:05:02Z     2026-10-25T07:03:02Z
spiffe://cluster.local/ns/part1-demo/sa/default      Root     Available     true           f1d9603bf93bcada5f16cdd5b361f9e4     2036-10-22T06:50:42Z     2026-10-25T06:50:42Z

  [
      {
>         "identity": "spiffe://cluster.local/ns/part1-demo/sa/default",
          "state": "Available",
          "certChain": [
      },

출력 결과 상세 해부:


Step 2. 침입자 파드(rogue-client), STRICT mTLS 인증, L7 인가 정책 배포

이제 Zero-Trust 보안을 검증하기 위해 3가지를 한 번에 배포합니다:

  1. PeerAuthentication (strict-mtls): part1-demo 네임스페이스 안의 모든 통신에 엄격한(STRICT) mTLS 암호화를 강제합니다.
  2. 침입자 계정(rogue-sa)과 파드(rogue-client): 해커가 클러스터 내부의 다른 파드(rogue-sa 계정 사용)를 장악해 order-api를 몰래 호출하는 상황을 재현합니다.
  3. L7 AuthorizationPolicy (order-api-zero-trust): order-api 서비스(targetRefs: Service/order-api) 앞단의 Waypoint Proxy에게 다음 규칙을 하달합니다:
    • 오직 cluster.local/ns/part1-demo/sa/default 신분증을 가진 파드만(principals),
    • 오직 GET 메서드로만(methods),
    • 단, /admin*으로 시작하는 관리자 경로는 제외하고(notPaths) 접근을 허용(ALLOW)하라! (나머지는 전부 자동 차단!)
# part4-zero-trust.yaml 파일 생성 및 클러스터 적용
@"
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: strict-mtls
  namespace: part1-demo
spec:
  mtls:
    mode: STRICT
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rogue-sa
  namespace: part1-demo
---
apiVersion: v1
kind: Pod
metadata:
  name: rogue-client
  namespace: part1-demo
  labels:
    app: rogue-client
spec:
  serviceAccountName: rogue-sa
  containers:
  - name: rogue-client
    image: curlimages/curl:8.10.1
    command: ["sleep", "3650d"]
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: order-api-zero-trust
  namespace: part1-demo
spec:
  targetRefs:
  - kind: Service
    group: ""
    name: order-api
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/part1-demo/sa/default"]
    to:
    - operation:
        methods: ["GET"]
        notPaths: ["/admin*"]
"@ | Set-Content -Path "$HOME\mesh-study\part4-zero-trust.yaml" -Encoding utf8

kubectl apply -f "$HOME\mesh-study\part4-zero-trust.yaml"
kubectl wait --for=condition=Ready pod/rogue-client -n part1-demo --timeout=90s

실제 출력 결과:

peerauthentication.security.istio.io/strict-mtls created
serviceaccount/rogue-sa created
pod/rogue-client created
authorizationpolicy.security.istio.io/order-api-zero-trust created
pod/rogue-client condition met
principals를 적을 때 주의할 점!

AuthorizationPolicy의 principals 칸에 SPIFFE ID를 적을 때는 맨 앞의 spiffe:// 접두사를 빼고 "cluster.local/ns/part1-demo/sa/default" 형태로 적는 것이 Istio의 문법 규칙입니다!

잠시 istioctl ztunnel-config certificates --node desktop-worker를 다시 쳐 보면, 새로 뜬 rogue-client 파드를 위해 spiffe://cluster.local/ns/part1-demo/sa/rogue-sa 인증서도 즉시 발급된 것을 볼 수 있습니다!


Step 3. 4대 출입 통제 시나리오 실측 검증 (200 OK vs 403 Forbidden)

이제 네 가지 요청을 차례대로 던져서 Waypoint Proxy가 우리의 Zero-Trust 헌법대로 정확히 통과/차단하는지 확인합니다.

# [시나리오 1] 정당한 신분(sa/default) + 허용된 메서드(GET) + 일반 경로(/orders) -> 200 OK 통과!
kubectl exec -n part1-demo study-client -- curl -s -w "`nHTTP_CODE: %{http_code}`n" http://order-api/orders | Select-Object -Last 6

# [시나리오 2] 정당한 신분(sa/default)이지만 금지된 관리자 경로(GET /admin/secret) -> 403 차단!
kubectl exec -n part1-demo study-client -- curl -s -w "`nHTTP_CODE: %{http_code}`n" http://order-api/admin/secret

# [시나리오 3] 정당한 신분(sa/default)이지만 허용되지 않은 메서드(DELETE /orders) -> 403 차단!
kubectl exec -n part1-demo study-client -- curl -s -X DELETE -w "`nHTTP_CODE: %{http_code}`n" http://order-api/orders

# [시나리오 4] 허가받지 않은 침입자 신분(sa/rogue-sa)의 접근(GET /orders) -> 403 차단!
kubectl exec -n part1-demo rogue-client -- curl -s -w "`nHTTP_CODE: %{http_code}`n" http://order-api/orders

실제 출력 결과:

# [시나리오 1 결과]
  "os": {
    "hostname": "order-api-86d8bb68dd-xh5rm"
  },
  "connection": {}
}
HTTP_CODE: 200

# [시나리오 2 결과]
RBAC: access denied
HTTP_CODE: 403

# [시나리오 3 결과]
RBAC: access denied
HTTP_CODE: 403

# [시나리오 4 결과]
RBAC: access denied
HTTP_CODE: 403
단 한 줄의 애플리케이션 코드 수정도 없이 완벽한 L7 RBAC 달성!

  • order-api 컨테이너(Node.js)는 인증서 검증 코드도, 권한 체크 코드도 전혀 갖고 있지 않습니다.
  • 하지만 앞단의 Waypoint Proxy 가 mTLS 인증서 속 SPIFFE 신분증과 HTTP 메서드, URL 경로를 대조하여 시나리오 2, 3, 4번을 모두 RBAC: access denied (HTTP_CODE: 403) 로 완벽히 막아냈습니다!


Step 4. 우회 공격 방어 — ztunnel의 L4 AuthorizationPolicy 직접 집행 검증

만약 똑똑한 해커(rogue-client)가 "어? 쿠버네티스 Service 도메인(http://order-api:80)으로 찌르니까 Waypoint Proxy가 막네? 그럼 Waypoint를 안 거치고 order-api 파드의 실제 IP 주소(10.244.x.x:8080)로 직접 찌르면 뚫리지 않을까?" 하고 시도하면 어떻게 될까요?

이를 방어하기 위해 파드 자체(selector.matchLabels: app: order-api)에 걸리는 L4 AuthorizationPolicy 를 추가하면, 목적지 워커 노드의 ztunnel이 L4(TCP/HBONE) 단계에서 연결 자체를 아예 끊어버립니다! 직접 확인해 봅시다.

# 1. order-api 파드를 직접 보호하는 L4 DENY 정책 생성 (ztunnel이 직접 집행!)
@"
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: order-api-l4-ztunnel
  namespace: part1-demo
spec:
  selector:
    matchLabels:
      app: order-api
  action: DENY
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/part1-demo/sa/rogue-sa"]
"@ | kubectl apply -f -

# 2. rogue-client가 Service(Waypoint)를 우회해 order-api 파드의 실제 IP(:8080)로 직접 접속 시도!
$orderPodIP = kubectl get pod -n part1-demo -l app=order-api -o jsonpath="{.items[0].status.podIP}"
kubectl exec -n part1-demo rogue-client -- curl -s -w "`nEXIT_CODE: %{exitcode}`n" --max-time 3 "http://${orderPodIP}:8080/orders"

# 3. ztunnel 로그에서 L4 정책 거부(policy rejection) 현장 포착!
kubectl logs -n istio-system -l app=ztunnel --tail=5 | Select-String "order-api-l4-ztunnel"

실제 출력 결과:

authorizationpolicy.security.istio.io/order-api-l4-ztunnel created

EXIT_CODE: 56
command terminated with exit code 56

2026-10-25T07:08:44.211072Z	error	access	connection complete	src.addr=10.244.2.9:52256 src.workload="rogue-client" src.namespace="part1-demo" src.identity="spiffe://cluster.local/ns/part1-demo/sa/rogue-sa" src.cluster="Kubernetes" dst.addr=10.244.2.6:15008 dst.hbone_addr=10.244.2.6:8080 dst.service="order-api.part1-demo.svc.cluster.local" dst.workload="order-api-86d8bb68dd-mkvrw" dst.namespace="part1-demo" dst.identity="spiffe://cluster.local/ns/part1-demo/sa/default" dst.cluster="Kubernetes" direction="inbound" bytes_sent=0 bytes_recv=0 duration="0ms" error="connection closed due to policy rejection: explicitly denied by: part1-demo/order-api-l4-ztunnel"

다음 파트 실습을 위한 보안 정책 정리

Part 05에서 자유롭게 카나리 배포와 서킷 브레이커 실험을 할 수 있도록, 방금 건 인가 정책 2개와 rogue-client를 깔끔히 정리합니다(strict-mtls는 그대로 유지해도 좋습니다!).

kubectl delete authorizationpolicy order-api-l4-ztunnel order-api-zero-trust -n part1-demo
kubectl delete pod rogue-client -n part1-demo

Step 5. 자주 발생하는 문제와 해결 (Troubleshooting)

증상 / 에러 메시지 원인 해결 방법
istioctl ztunnel-config certificates 실행 시 Error: ztunnel pod name or --node must be set 발생 클러스터에 노드가 3대이므로 어느 노드의 ztunnel 메모리를 볼지 지정해야 함 istioctl ztunnel-config certificates --node desktop-worker 처럼 노드 이름을 지정
AuthorizationPolicy에 principals: ["spiffe://cluster.local/..."]로 적었더니 허용이 안 되고 403이 뜸 Istio AuthorizationPolicy의 principals 필드는 spiffe:// 스킴을 제외한 나머지 문자열을 매칭함 앞의 spiffe://를 빼고 "cluster.local/ns/part1-demo/sa/default" 로 작성
Waypoint가 없는 상태에서 AuthorizationPolicy에 methods: ["GET"]을 넣었더니 모든 요청이 막힘 ztunnel은 L4 프록시라 HTTP 메서드를 읽을 수 없어 보안상 안전하게 전체 차단(Fail-Close)함 L7 조건을 쓰려면 targetRefs로 Service를 지정하고 해당 네임스페이스/서비스에 Waypoint를 활성화

Step 6. 이번 파트 핵심 명령어 정리 (Command Reference)

# 1. 특정 워커 노드의 ztunnel이 메모리에 들고 있는 SPIFFE 인증서(SVID) 목록 확인
istioctl ztunnel-config certificates --node desktop-worker

# 2. 네임스페이스 내의 PeerAuthentication 및 AuthorizationPolicy 목록 조회
kubectl get peerauthentication,authorizationpolicy -n part1-demo

# 3. ztunnel에서 L4 RBAC 정책 적용 및 차단 로그 확인
kubectl logs -n istio-system -l app=ztunnel --tail=20 | Select-String "policy rejection"

Step 7. 셀프 체크 (Self-Check Q&A)

Q1. 쿠버네티스 환경에서 출발지 파드의 IP 주소(10.244.x.x) 대신 SPIFFE ID를 기준으로 접근 제어를 해야 하는 이유는 무엇인가요?

Q2. 특정 서비스에 action: ALLOW인 AuthorizationPolicy를 딱 1개 추가하면, 그 정책에 언급되지 않은 다른 파드들의 요청은 어떻게 처리되나요?

Q3. Ambient Mesh에서 Waypoint Proxy가 집행하는 L7 차단과 ztunnel이 집행하는 L4 차단은 클라이언트 입장에서 어떤 결과 차이를 보이나요?


🔗 다음 파트로 이동

이제 우리 메시는 사이드카 없이도 강력한 Zero-Trust 보안(STRICT mTLS + L4/L7 RBAC) 까지 갖추었습니다!
다음 Part 05에서는 서비스 메시의 꽃이라 불리는 L7 트래픽 엔지니어링(90:10 카나리 배포, 헤더 기반 라우팅, 결함 주입)과 서킷 브레이커(OutlierDetection & UO) 를 실습합니다.


네비게이션

이전 목록 다음
◀ Part 04 이론 목록으로 Part 05 이론 ▶